package keyvaluestore

import (
	"io/ioutil"
	"os"
	"path/filepath"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/core"
	"github.com/pkg/errors"
)

const (
	newDirMode  = 0700
	newFileMode = 0600
)

type KeySerializer func(key interface{}) (string, error)

type Marshaller func(value interface{}) ([]byte, error)

type Unmarshaller func(value []byte) (interface{}, error)

type FileKeyValueStore struct {
	path          string
	keySerializer KeySerializer
	marshaller    Marshaller
	unmarshaller  Unmarshaller
}

type FileKeyValueStoreOptions struct {
	Path          string
	KeySerializer KeySerializer
	Marshaller    Marshaller
	Unmarshaller  Unmarshaller
}

func defaultMarshaller(value interface{}) ([]byte, error) {
	if value == nil {
		return nil, nil
	}
	valueBytes, ok := value.([]byte)
	if !ok {
		return nil, errors.New("converting value to byte array failed")
	}
	return valueBytes, nil
}

func defaultUnmarshaller(value []byte) (interface{}, error) {
	return value, nil
}

func (fkvs *FileKeyValueStore) GetPath() string {
	return fkvs.path
}

func New(opts *FileKeyValueStoreOptions) (*FileKeyValueStore, error) {
	if opts == nil {
		return nil, errors.New("FileKeyValueStoreOptions is nil")
	}
	if opts.Path == "" {
		return nil, errors.New("FileKeyValueStore path is empty")
	}
	if opts.KeySerializer == nil {
		opts.KeySerializer = func(key interface{}) (string, error) {
			keyString, ok := key.(string)
			if !ok {
				return "", errors.New("converting key to string failed")
			}
			return filepath.Join(opts.Path, keyString), nil
		}
	}
	if opts.Marshaller == nil {
		opts.Marshaller = defaultMarshaller
	}
	if opts.Unmarshaller == nil {
		opts.Unmarshaller = defaultUnmarshaller
	}
	return &FileKeyValueStore{
		path:          opts.Path,
		keySerializer: opts.KeySerializer,
		marshaller:    opts.Marshaller,
		unmarshaller:  opts.Unmarshaller,
	}, nil
}

func (fkvs *FileKeyValueStore) Load(key interface{}) (interface{}, error) {
	file, err := fkvs.keySerializer(key)
	if err != nil {
		return nil, err
	}
	if _, err1 := os.Stat(file); os.IsNotExist(err1) {
		return nil, core.ErrKeyValueNotFound
	}
	bytes, err := ioutil.ReadFile(file) // nolint: gas
	if err != nil {
		return nil, err
	}
	if bytes == nil {
		return nil, core.ErrKeyValueNotFound
	}
	return fkvs.unmarshaller(bytes)
}

func (fkvs *FileKeyValueStore) Store(key interface{}, value interface{}) error {
	if key == nil {
		return errors.New("key is nil")
	}
	if value == nil {
		return errors.New("value is nil")
	}
	file, err := fkvs.keySerializer(key)
	if err != nil {
		return err
	}
	valueBytes, err := fkvs.marshaller(value)
	if err != nil {
		return err
	}
	err = os.MkdirAll(filepath.Dir(file), newDirMode)
	if err != nil {
		return err
	}
	return ioutil.WriteFile(file, valueBytes, newFileMode)
}

func (fkvs *FileKeyValueStore) Delete(key interface{}) error {
	if key == nil {
		return errors.New("key is nil")
	}
	file, err := fkvs.keySerializer(key)
	if err != nil {
		return err
	}
	_, err = os.Stat(file)
	if err != nil {
		if !os.IsNotExist(err) {
			return errors.Wrapf(err, "stat dir failed")
		}
		// Doesn't exist, OK
		return nil
	}
	return os.Remove(file)
}
